package com.imooc.follow.service;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.imooc.commons.constant.ApiConstant;
import com.imooc.commons.constant.PointTypesConstant;
import com.imooc.commons.exception.ParameterException;
import com.imooc.commons.model.domain.ResultInfo;
import com.imooc.commons.model.vo.SignInDinerInfo;
import com.imooc.commons.utils.AssertUtil;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.StrUtil;

/**
 * \* @author: bear
 * \* Date: 2021-08-12-14:01
 * \* describe:
 */
@Service
public class SignService {

	@Value("${service.name.ms-oauth-server}")
	private String oauthServerName;
	@Value("${service.name.ms-points-server}")
	private String pointsServerName;

	@Resource
	private RestTemplate restTemplate;
	@Resource
	private RedisTemplate redisTemplate;

	/**
	 * 用户签到
	 *
	 * @param accessToken 登录用户 token
	 * @param dateStr 查询的日期，默认当天 yyyy-MM-dd
	 *
	 * @return 连续签到次数
	 */
	public int doSign(String accessToken, String dateStr) {
		// 获取登录用户信息
		SignInDinerInfo signInDinerInfo = loadSignInDinerInfo(accessToken);
		//获取日期
		Date date = getDate(dateStr);
		//获取日期对应的天数，多少号
		int offset = DateUtil.dayOfMonth(date) -1 ; //因为bitmap是从0开始，0代表一号，所以减1
		//构建key
		String signKey = buildSignKey(signInDinerInfo.getId(),date);
		//查看是否已经签到
		Boolean isSigned = redisTemplate.opsForValue().getBit(signKey, offset);
		AssertUtil.isTrue(isSigned,"当前日期已经完成签到，无需在签到");
		//签到
		redisTemplate.opsForValue().setBit(signKey,offset,true);
		//统计连续签到次数
		int count = getContinuousSignCount(signInDinerInfo.getId(),date);
		//添加积分（可以使用消息队列进行性能优化）
		int points = addPoints(count,signInDinerInfo.getId());
		return points;
	}

	/**
	 * 统计某月连续签到次数
	 * @param dinerId  用户id
	 * @param date   日期
	 * @return   当月连续签到次数
	 */
	private int getContinuousSignCount(Integer dinerId, Date date) {
		//获取日期对应的天数，多少号
		int dayOfMonth = DateUtil.dayOfMonth(date);
		//构建key
		String signKey = buildSignKey(dinerId,date);
		//u为无符号，i为有符号
		// bitfield w get u4 0    #从w的第一个位开始取4个位(0110)，结果为无符号数(u)
		// bitfield w get u3 2    #从w的第三个位开始取3个位(101)，结果为无符号数(u)
		//命令biffield key get [u/i] offset value;
		// 此命令就是get取出key对应的位图，指定value索引开始，取offset位偏移量的二进制
		// bitfield key get u31 0 取出0-31天的所有数据，结果为十进制
		BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
				.get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
				.valueAt(0);
		List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
		if(list == null || list.isEmpty()) return 0;
		int signCount = 0;
		long v = list.get(0) == null ? 0:list.get(0);
		//取低位连续部位0的个数，即为连续签到的次数，考虑当天尚未签到的情况
		for (int i = dayOfMonth;i>0;i--){
			//右移，再左移，如果等于自己说明最低位是0，表示未签到
			if(v>>1 <<1 == v){
				//低位为0，且非当天说明联系签到中断了
				if(i != dayOfMonth) break;
			}else {
				//签到了，签到数加一。
				signCount ++;
			}
			//右移一位并重新赋值，相当于把最右边一位去除
			v >>=1;
		}
		return signCount;
	}

	private String buildSignKey(Integer id, Date date) {
		return String.format("user:sign:%d:%s",id,DateUtil.format(date,"yyyyMM"));
	}

	/**
	 * 获取登录用户信息
	 *
	 * @param accessToken
	 *
	 * @return
	 */
	private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
		// 登录校验
		AssertUtil.mustLogin(accessToken);
		// 获取登录用户信息
		String url = oauthServerName + "user/me?access_token={accessToken}";
		ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
		if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
			throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
		}
		// 这里的data是一个LinkedHashMap，SignInDinerInfo
		SignInDinerInfo dinerInfo = BeanUtil
				.fillBeanWithMap((LinkedHashMap)resultInfo.getData(), new SignInDinerInfo(), true);
		if (dinerInfo == null) {
			throw new ParameterException(ApiConstant.NO_LOGIN_CODE, ApiConstant.NO_LOGIN_MESSAGE);
		}
		return dinerInfo;
	}

	/**
	 * 获取日期
	 *
	 * @param dateStr yyyy-MM-dd 默认当天
	 *
	 * @return
	 */
	private static Date getDate(String dateStr) {
		if (StrUtil.isBlank(dateStr)) {
			return new Date();
		}
		try {
			return DateUtil.parse(dateStr);
		} catch (Exception e) {
			throw new ParameterException("请传入yyyy-MM-dd的日期格式");
		}
	}

	/**
	 * 获取用户签到次数
	 *
	 * @param accessToken 登录token
	 * @param dateStr     查询的日期，默认当月 yyyy-MM-dd
	 * @return 当前的签到次数
	 */
	public long getSignCount(String accessToken ,String dateStr){
		//获取用户信息
		SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
		//获取日期
		Date date = getDate(dateStr);
		//构建key
		String signKey = buildSignKey(dinerInfo.getId(),date);
		//e.g bitcount user:sign:5:202011
		return (Long)redisTemplate.execute(
				(RedisCallback<Long>) con ->
				con.bitCount(signKey.getBytes()));
	}

	/**
	 * 获取当月签到情况
	 *
	 * @param accessToken 登录token
	 * @param dateStr     查询的日期，默认当月 yyyy-MM-dd
	 * @return Key为签到日期，Value为签到状态的Map
	 */

	public Map<String,Boolean> getSignInfo(String accessToken,String dateStr){
		// 获取登录用户信息
		SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
		//获取日期
		Date date = getDate(dateStr);
		// 		构建Redis保存的Key
		String signKey = buildSignKey(dinerInfo.getId(),date);
		// 获取月份的总天数（考虑2月闰、平年）
		int dayofMonth = DateUtil.lengthOfMonth(DateUtil.month(date) +1,
				DateUtil.isLeapYear(DateUtil.year(date)));
		// 通过BITFIELD指令获取当前月的所有签到数据
		BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
				.get(BitFieldSubCommands.BitFieldType.unsigned(dayofMonth))
				.valueAt(0);
		List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
		// 构建一个自动排序的 Map
		Map<String, Boolean> signInfo = new TreeMap<>();
		// 遍历进行判断是否签到，并存入TreeMap方便排序
		long v = list.get(0) == null ? 0 : list.get(0);
		for (int i = dayofMonth;i>0;i--){
			// 获取日期时间，比如 i = 31，最终拿到 yyyyMM31
			LocalDateTime localDateTime = LocalDateTimeUtil.of(date).withDayOfMonth(i);
			// 先右移一位再左移一位，如果还不变那只能证明低位是 0，否则低位就是 1
			Boolean flag = v>>1<<1 !=v;
			signInfo.put(DateUtil.format(localDateTime,"yyyy-MM-dd"),flag);
			v>>=1;
		}
		// 由低位到高位进行遍历，为 0 表示未签，为 1 表示已签
		return signInfo;
	}


	/**
	 * 添加用户积分
	 *
	 * @param count 连续签到次数
	 * @param signInDinerId 登录人id
	 *
	 * @return
	 */
	public int addPoints(int count, Integer signInDinerId) {
		//签到1天送10积分，连续签到两天送20积分，3天送30积分，4天以上送50积分
		int point = 10;
		if (count == 2) {
			point = 20;
		} else if (count == 3) {
			point = 30;
		} else if (count >= 4) {
			point = 50;
		}
		//调用积分接口添加积分
		//构建请求头
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
		//构建请求体，请求参数
		MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
		body.add("dinerId", signInDinerId);
		body.add("points", point);
		body.add("types", PointTypesConstant.sign.getType());
		HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
		//发送请求
		ResponseEntity<ResultInfo> result = restTemplate.postForEntity(pointsServerName+"/points", entity, ResultInfo.class);
		AssertUtil.isTrue(result.getStatusCode() != HttpStatus.OK, "积分新增失败");
		ResultInfo resultInfo = result.getBody();
		if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
			//失败了要回滚数据
			throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
		}
		return point;
	}
}
